//------------------------------------------------------------
// Microsoft?XNA Game Studio Creator's Guide  
// by Stephen Cawood and Pat McGee 
// Copyright (c) McGraw-Hill/Osborne. All rights reserved.
// http://www.mhprofessional.com/product.php?isbn=007149071X
//------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
using System.IO;
using MD2Importer;

namespace quakeMD2
{
    public class MD2
    {
        Quake2Model md2;
        private VertexPositionNormalTexture[] modelVerts;
        private Texture2D mTexMd2;
        private int miNumFaces;

        //------------------------------------------------------------
        // M O D U L E   D E C L A R A T I O N S
        //------------------------------------------------------------
        private float mfFrameTime;
        private float mfPrevTime;
        private float mfAnimSpeed = 5.0f;
        public VertexBuffer vertexBuffer;
        private int miFrameNum = 0;
        private int miNextFrameNum = 1;
        private int miAnimNum;
        private bool mbSequenced = false;
        private int mi2ndAnimSequence;
        private bool mbPaused = false;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public MD2 ()
        { }

        /// <summary>
        /// Sets value to scale the animation time.
        /// </summary>
        /// <param name="fSpeed">Amount of time scale.</param>
        public void setAnimSpeed ( float fSpeed )
        {
            mfAnimSpeed = fSpeed;
        }

        /// <summary>
        /// Total triangles in md2 model. This is the same amount for each frame.
        /// </summary>
        public int getNumTriangles ()
        {
            return miNumFaces;
        }

        /// <summary>
        /// Returns the texture that skins the model.
        /// </summary>
        public Texture2D getTexture ()
        {
            return mTexMd2;
        }

        /// <summary>
        /// Initiates loading of the model through the content pipeline using
        /// the content importer and processor.  Also loads texture through
        /// content pipeline. (Must use an XNA friendly texture format like
        /// *.tga, *.png, *.bmp, *.jpg. Native Quake *.pcx files won't load.
        /// </summary>
        /// <param name="gfx">Graphics device handle needed to initialize vertex buffer.</param>
        /// <param name="strModel">Name of model and file path.</param>
        /// <param name="strModel">Name of texture and file path.</param>
        /// <param name="strModel">Content manager object for loading through pipeline.</param>
        public void loadModel ( GraphicsDevice gfx, string strModel,
                              string strSkin, ContentManager content )
        {
            md2 = content.Load<Quake2Model>( strModel );
            int qq = md2.miSize;
            miNumFaces = md2.miNumFaces;
            mTexMd2 = content.Load<Texture2D>( strSkin );
            initializeVertexBuffer( gfx );
        }

        /// <summary>
        /// Sets up dynamic storage variable for xyz, normal, and texture data. A dynamic
        /// area is needed to update the vertex buffer.
        /// </summary>
        /// <param name="gfx">Graphics device handle needed to initialize vertex buffer.</param>
        private void initializeVertexBuffer ( GraphicsDevice gfx )
        {
            modelVerts = new VertexPositionNormalTexture[md2.miNumFaces * 3];
            vertexBuffer = new VertexBuffer( gfx,
                                            VertexPositionNormalTexture.SizeInBytes * modelVerts.Length,
                                            BufferUsage.None );
        }

        /// <summary>
        /// Sets current and next frame in current animation sequence.
        /// </summary>
        /// <param name="animNum">Current animation.</param>
        private void setCurrentAndNextFrames ( int animNum )
        {
            // get start and end frames of animation
            int iEndFrame = md2.miEndFrame[animNum];
            int iStartFrame = md2.miStartFrame[animNum];

            // set current frame
            miFrameNum = iStartFrame;

            // set next frame - if animation more than 1
            // the nextFrame = currentFrame + 1 else nextFrame = currentFrame
            if (iEndFrame > iStartFrame)
                miNextFrameNum = miFrameNum + 1;
            else
                miNextFrameNum = miFrameNum;
        }

        /// <summary>
        /// Let's user select the animation. ie. Walk, run, jump, crouch, crawl etc.
        /// </summary>
        /// <param name="iAnim">Animation identifier.</param>
        public void setAnim ( int iAnim )
        {
            miAnimNum = iAnim;
            setCurrentAndNextFrames( miAnimNum );
        }

        /// <summary>
        /// Let's user select the animation. ie. Walk, run, jump, crouch, crawl etc.
        /// </summary>
        /// <param name="iAnim">Animation identifier.</param>
        public void setAnimSequence ( int i1stAnim, int i2ndAnim )
        {
            mbSequenced = true;
            mi2ndAnimSequence = i2ndAnim;
            setAnim( i1stAnim );
        }

        /// <summary>
        /// Let's user determine if a specific animation is playing
        /// </summary>
        /// <param name="iAnimNum">Animation identifier.</param>
        public bool isPlaying ( int iAnimNum )
        {
            if (iAnimNum == miAnimNum)
                return true;
            return false;
        }

        /// <summary>
        /// Goes to next animation in list. Starts back at beginning
        /// when the end of the list is reached.
        /// </summary>
        public void advanceAnimation ()
        {
            // go to next animation in array
            if (miAnimNum + 1 < md2.miNumAnim)
                miAnimNum += 1;
            else
                miAnimNum = 0;
            setCurrentAndNextFrames( miAnimNum );
        }

        /// <summary>
        /// Sets class level variable, mbPaused, to freeze the animation.
        /// </summary>
        public void Pause ()
        {
            mbPaused = true;
        }

        /// <summary>
        /// Allows user to start animation from paused state.
        /// </summary>
        public void Resume ()
        {
            mbPaused = false;
        }

        /// <summary>
        /// Moves to next frame in current animation when time is up
        /// for the first animation.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        private bool advanceFrame ( GameTime gameTime )
        {
            // don't advance frame if paused
            if (mbPaused)
                return false;

            float fFrameTime = (float)gameTime.ElapsedGameTime.Milliseconds / 20.0f;

            mfFrameTime += fFrameTime;
            mfFrameTime = mfFrameTime % mfAnimSpeed;

            // frame has been completed
            if (mfFrameTime < mfPrevTime)
            {
                int iEndFrame = md2.miEndFrame[miAnimNum];
                int iStartFrame = md2.miStartFrame[miAnimNum];

                // start at beginning frame if all frames completed
                if (miFrameNum < iEndFrame - 1)
                {
                    miFrameNum += 1;
                    miNextFrameNum = miFrameNum + 1;

                    if (miNextFrameNum > iEndFrame - 1)
                        miNextFrameNum = iStartFrame;
                }

                // animation is over and this anim is in a sequence
                // so advance to next animation in sequence
                else if (mbSequenced == true)
                {
                    mbSequenced = false;
                    setAnim( mi2ndAnimSequence );
                }
                // animation is continuous so start frame at beginning
                else
                {
                    miFrameNum = iStartFrame;
                    miNextFrameNum = iStartFrame + 1;
                }
            }
            mfPrevTime = mfFrameTime;
            return true;
        }

        /// <summary>
        /// Updates animation time for the model. Interpolates
        /// xyz's, and normals. Resets vertex buffer with 
        /// updated model updated xyz, updated normal's, and 
        /// uv's (uv's are the same for all frames).
        /// </summary>
        /// <param name="gfx">Handle to graphics device for updating vertex buffer.</param>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public void updateModel ( GraphicsDevice gfx, GameTime gameTime )
        {
            Vector2 v2UV; Vector3 v3Pos; Vector3 v3Norm; Vector3 v3Cur, v3Next;
            Vector3 v3NewNorm, v3NewNormNext;
            v3Norm = new Vector3( 0.0f, 1.0f, 0.0f );

            int vertCount = 0;

            float fTimeScale = mfFrameTime / mfAnimSpeed;

            v3Norm = new Vector3( 0.0f, 1.0f, 0.0f );
            for (int i = 0; i < md2.miNumFaces; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    int posIndex = md2.miPosIndex[miFrameNum, i, j];
                    int texIndex = md2.miFrameUVIndex[miFrameNum, i, j];

                    v2UV = new Vector2( md2.mfUV[miFrameNum, texIndex, 0],
                                           md2.mfUV[miFrameNum, texIndex, 1] );

                    v3Cur = new Vector3( md2.mfFramePos[miFrameNum, posIndex, 0],
                                           md2.mfFramePos[miFrameNum, posIndex, 1],
                                           md2.mfFramePos[miFrameNum, posIndex, 2] );

                    v3Next = new Vector3( md2.mfFramePos[miNextFrameNum, posIndex, 0],
                                           md2.mfFramePos[miNextFrameNum, posIndex, 1],
                                           md2.mfFramePos[miNextFrameNum, posIndex, 2] );

                    v3NewNorm = new Vector3( md2.mfNormal[miFrameNum, posIndex, 0],
                                           md2.mfNormal[miFrameNum, posIndex, 1],
                                           md2.mfNormal[miFrameNum, posIndex, 2] );

                    v3NewNormNext = new Vector3( md2.mfNormal[miNextFrameNum, posIndex, 0],
                    md2.mfNormal[miNextFrameNum, posIndex, 1],
                    md2.mfNormal[miNextFrameNum, posIndex, 2] );

                    // project (interpolate) xyz by time fraction between frames
                    v3Pos = v3Cur + (v3Next - v3Cur) * fTimeScale;
                    v3Norm = v3NewNorm + (v3NewNormNext - v3NewNorm) * fTimeScale;

                    modelVerts[vertCount++]
                           = new VertexPositionNormalTexture( v3Pos, v3Norm, v2UV );
                }
            }
            vertexBuffer.SetData<VertexPositionNormalTexture>( modelVerts );
            advanceFrame( gameTime );
        }
    }
}